/*
 * LetMeFly performs the following:
 * - Jumps to fly when mounted and running on the ground
 * - Adjusts hotspots of current profile a few yards off ground
 * - Returns to ground when stuck or when POI is nearby
 * - Uses Flight Form and Swift Flight Form when available
 * 
 * Author: lofi
 */

using Styx.Helpers;
using Styx.Logic;
using Styx.Logic.BehaviorTree;
using Styx.Logic.Combat;
using Styx.Logic.POI;
using Styx.Logic.Pathing;
using Styx.Logic.Profiles;
using Styx.Plugins.PluginClass;
using Styx.WoWInternals.WoWObjects;
using Styx.WoWInternals;
using Styx;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading;
using System.Windows.Forms;
using System.Xml.Linq;
using System;

namespace lofi2
{
    public class LetMeFly : HBPlugin, IDisposable
    {
        // User configurations
        private double JumpHoldTime = 0.1; // How long to hold jump (scaled based on flight speed)
        private double JumpCooldown = 0.2; // Minimum time between jumps (in seconds)
        private double JumpDistance = 10; // Minimum distance traveled between jumps
        private double SafeDistance = 40; // Do not jump when this close to POI
        private double ZAxisModifier = 1.0; // Adjust hotspots vertically
        private double StuckDistance = 10; // Reset mounted still timer after moving this far
        private double MountedStillTimeout = 30; // Dismount when mounted still for this long (in seconds)
        private double FlightFormDistance = 50; // Force mount when POI is further than this many yards
        private double FlightFormCooldown = 10; // Minimum time to re-cast after dismounting (in seconds)

        public override string Name { get { return "LetMeFly"; } }
        public override string Author { get { return "lofi"; } }
        public override Version Version { get { return new Version(3, 5, 1); } }
        public override bool WantButton { get { return false; } }
        public override string ButtonText { get { return Version.ToString(); } }
        private LocalPlayer Me { get { return ObjectManager.Me; } }
        private Stopwatch lastJumpTimer = new Stopwatch();
        private Stopwatch mountedStillTimer = new Stopwatch();
        private Stopwatch lastMountedTimer = new Stopwatch();
        private WoWPoint lastPoint = new WoWPoint();
        private WoWPoint lastJumpPoint = new WoWPoint();
        private EventHandler<MountUpEventArgs> mountHandler = null;
        private string tempFilePath = null;

        public override void OnButtonPress()
        {
        }

        public override void Pulse()
        {
            try
            {
                if (Me == null || !ObjectManager.IsInGame || TreeRoot.Current == null || BotPoi.Current == null
                    || (!TreeRoot.Current.Name.Contains("Grind") && !TreeRoot.Current.Name.Contains("Quest")))
                {
                    return; // sanity check and disable except for Grind bot / Questing
                }

                if (ReloadProfile())
                {
                    return;
                }

                if (DetectStuck())
                {
                    return;
                }

                if (Me.Mounted)
                {
                    lastMountedTimer.Reset();
                }
                else if (!lastMountedTimer.IsRunning)
                {
                    lastMountedTimer.Start();
                }

                if (BotPoi.Current.Location.Distance(Me.Location) < SafeDistance)
                {
                    if (Me.IsFlying && !Me.IsMoving)
                    {
                        Log("Moving to ground - not moving with POI nearby!");
                        ReturnToGround();
                    }
                }
                else if (!Me.Mounted && Me.IsMoving && BotPoi.Current.Location.Distance(Me.Location) > FlightFormDistance && lastMountedTimer.Elapsed.TotalSeconds > FlightFormCooldown)
                {
                    UseFlightForm();
                    lastMountedTimer.Reset();
                }
                else if (Me.Mounted && Me.IsMoving && !Me.IsFlying && CanFly() && lastJumpTimer.Elapsed.TotalSeconds > JumpCooldown)
                {
                    WoWPoint jumpPoint = new WoWPoint(Me.X, Me.Y, Me.Z);

                    if (jumpPoint.Distance(lastJumpPoint) > 15)
                    {
                        Log("Jumping to fly!");
                        WoWMovement.Move(WoWMovement.MovementDirection.JumpAscend, TimeSpan.FromSeconds(JumpHoldTime / GetFlightSpeed()));
                        lastJumpPoint = jumpPoint;
                    }

                    lastJumpTimer.Reset();
                    lastJumpTimer.Start();
                }
            }
            catch (Exception e)
            {
                Log("ERROR: " + e.Message + ". See debug log.");
                Logging.WriteDebug("LetMeFly exception:");
                Logging.WriteException(e);
            }
        }

        private bool UseFlightForm()
        {
            if (SpellManager.CanCast(40120)) // Swift Flight Form
            {
                Log("Casting Flight Form!");
                SpellManager.Cast(40120);
                return true;
            }
            else if (SpellManager.CanCast(33943)) // Flight Form
            {
                Log("Casting Swift Flight Form!");
                SpellManager.Cast(33943);
                return true;
            }

            return false;
        }

        private bool ReloadProfile()
        {
            // check if profile has changed
            if (File.Exists(ProfileManager.XmlLocation) && !ProfileManager.XmlLocation.Equals(tempFilePath) && CanFly())
            {
                string pattern = @"<Hotspot\s+X=""([0-9.-]+)""\s+Y=""([0-9.-]+)""\s+Z=""([0-9.-]+)""";
                Regex matcher = new Regex(pattern);

                StreamReader reader = null;
                StreamWriter writer = null;
                string line = null;
                try
                {
                    reader = new StreamReader(ProfileManager.XmlLocation);
                    tempFilePath = ProfileManager.XmlLocation.Remove(ProfileManager.XmlLocation.Length - 4, 4) + ".fly.xml";
                    writer = new StreamWriter(tempFilePath, false);
                    while ((line = reader.ReadLine()) != null)
                    {
                        Match m = matcher.Match(line);
                        if (m.Success)
                        {
                            double z = Double.Parse(m.Groups[3].Value) + ZAxisModifier;
                            writer.WriteLine("<Hotspot X=\"{0}\" Y=\"{1}\" Z=\"{2}\" />", m.Groups[1], m.Groups[2], z);
                        }
                        else
                        {
                            writer.WriteLine(line); // don't edit this line
                        }
                    }
                }
                catch (Exception e)
                {
                    Log("Failed to modify hotspots in profile! See debug log.");
                    Logging.WriteDebug("LetMeFly exception:");
                    Logging.WriteException(e);
                }
                finally
                {
                    if (reader != null) reader.Close();
                    if (writer != null) writer.Close();
                }

                if (tempFilePath != null)
                {
                    ProfileManager.LoadNew(tempFilePath);
                    File.Delete(tempFilePath);
                    Log("Reloaded profile with modified hotspots");
                    return true;
                }
            }

            return false;
        }

        private bool CanFly()
        {
            if (Me.Combat || !Me.IsAlive)
                return false;

            if (!SpellManager.HasSpell(90265) && !SpellManager.HasSpell(34091) && !SpellManager.HasSpell(34090))
                return false; // no level of flying has not been trained

            if ((Me.MapId == 0 || Me.MapId == 1) && !SpellManager.HasSpell(90267))
                return false; // in azeroth without "flight master's license"

            if (Me.MapId == 571 && !SpellManager.HasSpell(54197))
                return false; // in northred without "cold weather flying"

            if (Me.IsInInstance || Me.IsIndoors || Battlegrounds.IsInsideBattleground)
                return false;

            return true;
        }

        private bool DetectStuck()
        {
            WoWPoint myPoint = new WoWPoint(Me.X, Me.Y, Me.Z);

            if (myPoint.Distance(lastPoint) > StuckDistance)
            {
                lastPoint = myPoint;
                mountedStillTimer.Reset();
                return false;
            }

            if (!mountedStillTimer.IsRunning && Me.Mounted)
            {
                mountedStillTimer.Start();
            }
            else if (mountedStillTimer.IsRunning && !Me.Mounted)
            {
                mountedStillTimer.Reset();
            }
            else if (mountedStillTimer.Elapsed.TotalSeconds > MountedStillTimeout)
            {
                if (Me.IsFlying)
                {
                    ReturnToGround();
                }
                return true;
            }

            return false;
        }

        private float GetDistanceToGround()
        {
            if (!Me.IsFlying)
                return 0;

            float ground = float.MinValue;
            List<float> zs = Navigator.HeightProvider.FindHeights(Me.X, Me.Y);
            foreach (float z in zs)
            {
                if (z > ground && z < Me.Z)
                {
                    ground = z;
                }
            }

            return Me.Z - ground;
        }

        private double GetFlightSpeed()
        {
            double speed = 1.0;
            if (SpellManager.HasSpell(90265))
            {
                speed += 3.1;
            }
            else if (SpellManager.HasSpell(34091))
            {
                speed += 2.8;
            }
            else if (SpellManager.HasSpell(34090))
            {
                speed += 1.5;
            }
            
            if (Me.HasAura("Crusader Aura"))
            {
                speed *= 1.2;
            }
            else if (SpellManager.HasSpell(78633)) // "Mount Up" guild perk
            {
                speed *= 1.1;
            }

            return speed;
        }

        private void ReturnToGround()
        {
            Log("Returning to ground");
            try
            {
                WoWMovement.Move(WoWMovement.MovementDirection.Descend);
                while (GetDistanceToGround() > 10)
                {
                    Thread.Sleep(100);
                    ObjectManager.Update();
                }
            }
            finally
            {
                WoWMovement.MoveStop(WoWMovement.MovementDirection.Descend);
            }

            Mount.Dismount();
        }

        private void MountHandler(object sender, MountUpEventArgs e)
        {
            if (UseFlightForm())
            {
                e.Cancel = true;
            }
        }

        public override void Initialize()
        {
            lastJumpTimer.Start();
            mountHandler = new EventHandler<MountUpEventArgs>(MountHandler);
            Mount.OnMountUp += mountHandler;
            Log("Loaded version " + Version);
        }

        public override void Dispose()
        {
            Mount.OnMountUp -= mountHandler;
        }

        private void Log(string format, params object[] args)
        {
            Logging.Write(Color.CadetBlue, "[LetMeFly] " + format, args);
        }
    }
}

